---
title: 03 代理
---

import FlagIcon from '@material-ui/icons/FlagSharp'
import Layer4Proxy from './layer-4-proxy.svg';
import Layer7Proxy from './layer-7-proxy.svg';
import DemuxHTTP from './demux-http.svg';
import MuxHTTP from './mux-http.svg';
import SvgProxyPipelines from './proxy-pipelines.svg';

我们已经把 Pipy 用于服务器来处理 HTTP 请求了。现在我们把它用于最常见的用途：网络代理。

## 创建一个代理

点击左上角的 Pipy 图标以返回 Web 界面主页。以 [Part 2](/tutorial/02-echo#创建一个-Pipy-代码库) 里相同的方式创建一个新的代码库，命名为 `/proxy`。

> 先不管我们之前创建的 `/hello` 代码库，暂时留着它，因为我们待会儿要用它作为我们的上游服务器，用来测试。

向这个新的代码库添加一个新文件 `/proxy.js`，通过点击 <FlagIcon/> 使它成为入口。`/proxy.js` 脚本看起来应该是这样的：

``` js
pipy()

  .listen(8000)
  .demuxHTTP().to('forward')

  .pipeline('forward')
  .muxHTTP().to('connection')

  .pipeline('connection')
  .connect('localhost:8080')
```

## 代码剖析

除了一个 _端口管道_ 布局，这次我们还添加了两个 _子管道_ 布局，它们分别叫 _"forward"_ 和 _"connection"_。

1. 这个端口管道布局监听 8000 端口。它只有一个过滤器 **demuxHTTP()**，类似于我们之前用过的接收 HTTP 请求和返回响应的 **serveHTTP()**，但不同的是，
它不是在一个 _回调函数_ 里面处理 HTTP 请求，而是在一个 _子管道_ 里面处理，该子管道是通过紧随其后的 **to()** 调用以名称 _"forward"_ 来引用的。

2. 名为 _"forward"_ 的 _子管道_ 布局只有一个过滤器 **muxHTTP()**。这个过滤器把它的输入消息放到另一个名为 _"connection"_ 的 _子管道_（也是通过紧随其后的 **to()** 方法调用来给定）里面 _排队_。该过滤器还从 _"connection" 子管道_ 的输出中 _出队_ 一个消息，成为该过滤器的输出。

3. 最后的管道布局 _"connection"_ 也是一个 _子管道_。它只包含一个 **connect()** 过滤器。该过滤器建立一个到远程主机 _"localhost:8080"_（在其构造参数里给定）的 TCP 连接。当连接建立后，该过滤器发送其输入数据给那个主机，同时输出从那个主机接收到的数据。

## 多路复用和解复用

你可能会问为什么需要 3 个管道布局，而不是像 _"Hello World"_ 那样仅仅一个就搞定。这个跟 _七层_ 协议（例如像 HTTP 那样的 _应用层_ 协议）相较于 _四层_ 协议（像 TCP 那样的 _传输层_ 协议）的工作方式有关。

简单地说，在四层我们看到的是连续的 _“流”_，而在七层我们看到的是离散的 _“消息”_。

对于一个四层代理，事情简单得多，因为到下游客户端的连接跟到上游服务器的链接是一对一关系。对于 Pipy 而言，这意味着处理上游的管道和处理下游的管道是一对一关系。在许多情况下，我们只需要一个位于客户端和服务器之间的单个管道，使其既连接客户端也连接服务器，就可以简单地代理一个四层流。

<div style="text-align: center">
  <Layer4Proxy/>
</div>

对于一个七层代理，事情变得复杂起来。一个处理下游连接的端口管道会接收到多个 _消息_，每条消息可能会去往不同的上游服务器。同样的，当来自两个不同下游的消息去往同一个服务器时，你可能倾向于让他们经由同一个 _共享_ 的上游连接以减少资源的使用。

<div style="text-align: center">
  <Layer7Proxy/>
</div>

因此，连接到下游客户端的管道和连接到上游服务器的管道是多对多关系。这就是为什么我们需要分开的管道布局来处理下游和上游。在一个面向下游的管道里，消息从单个传输流中被 _“解复用（demux）”_ 出来。在一个面向上游的管道里，消息被 _“多路复用（mux）”_ 到单个传输流中。

### 过滤器: demuxHTTP()

在我们的例子里，一个 _端口管道_ 以 [_Data_](/reference/api/Data) 事件序列的形式吞下收到的数据，每个事件持有一小块来自 TCP 流的数据块。**demuxHTTP()** 所做的就是把 TCP 流 _“解码”_ 和 _“解复用”_ 成独立的 HTTP 消息，以一对 [MessageStart](/reference/api/MessageStart) 和 [MessageEnd](/reference/api/MessageEnd) 事件包围后，把他们各自喂给一个独占的子管道进行处理。

<div style="text-align: center">
  <DemuxHTTP/>
</div>

故事到此还没有结束，每个子管道最终将输出一个消息作为响应。来自所有子管道末端的输出消息被 **demuxHTTP()** 采集，他们被 _“编码”_ 并 _“多路复用”_ 到单个 TCP 流，然后返回客户端。

正如你所见，虽然过滤器叫做 _"demuxHTTP"_，但其实它同时执行了 _解复用_ 和 _多路复用_ 的工作，在请求路径上解复用，在响应路径上多路复用。

### 过滤器: muxHTTP()

相对于 **demuxHTTP()** 的是 **muxHTTP()**。跟 **demuxHTTP()** 产生多个子管道不同，多个 **muxHTTP()** 实例会把消息 _合并_ 到一个 _共享_ 子管道里。

<div style="text-align: center">
  <MuxHTTP/>
</div>

该过滤器接受一个可选的回调函数 `target` 用于指定它往哪个子管道合并消息。当两个 **muxHTTP()** 实例 _目标_ 相同时，它们就会 _合并_ 到同一个 HTTP 会话。换而言之，来自两个 **muxHTTP()** 实例的请求被 _多路复用_ 到同一个子管道内的一个 _共享_ HTTP 流。

`target` 回调可以返回任意类型的值。它返回的任何值都会成为用于引用某个共享子管道的 _键_。当一个键的子管道不存在时，新的子管道会被创建，并且被其他后来也引用到同一个键的 **muxHTTP()** 实例所共享。

在未给定 `target` 回调时，这个 _键_ 默认为 `__inbound`。`__inbound` 是一个内置的 _上下文变量_，代表当前的下游连接。所以，默认情况下，源自同一个客户端连接的全部 **muxHTTP()** 过滤器只会创建一个子管道并向它合并消息。若要改变策略，可以向该过滤器的第一个参数提供一个定制的 `target` 回调。

> 注意，这里的 "合并" 只发生在来自同一个管道布局里同一个位置上的 **muxHTTP()** 实例之间。在两个不同的管道布局之间，或者同一个管道布局的两个不同位置之间，**muxHTTP()** 实例是绝不会合并到同一个子管道的。

像 **demuxHTTP()** 一样，虽然名字叫 “muxHTTP”，但其实它在 _多路复用_ 输入请求之外，还 _解复用_ 从共享子管道末端出来的响应。

### 管道拓扑

在我们的例子里，假设我们有 1 个客户端连接，跑了 2 个请求，去往同一个服务器。在运行时，为了处理这些，我们共需要 4 个相互连接的管道：

- 1 个面向下游的端口管道以连接到客户端
- 1 个面向上游的 _"connection"_ 子管道以连接到服务器
- 2 个 _"forward"_ 子管道分别传递 2 个请求

<div style="text-align: center">
  <SvgProxyPipelines/>
</div>

## 匿名管道

在上面的脚本中，我们增加了两个子管道布局，分别命名为 “forward” 和 “connection”。这些名字或许可以帮助解释这些管道的用途。然而，随着脚本逐渐增长，为每个子管道布局起一个有意义的名字会成为一种负担。在很多情况下，仅仅把子管道布局嵌入到母过滤器里而不需要名字的话，反而更简洁，也更容易理解。

为了达到这个目的，我们可以像下面这样修改代码：

``` js
pipy()

  .listen(8000)
  .demuxHTTP().to(
    $=>$.muxHTTP().to(
      $=>$.connect('localhost:8080')
    )
  )
```

我们不是给方法 **to()** 一个名字，而是给它一个函数。这个函数仅有一个参数，可以通过调用它上面的各种方法向正在嵌入的子管道布局添加过滤器，就像从 [pipy()](/reference/api/pipy) 返回的 [Configuration](/reference/api/Configuration) 对象一样。唯一的不同是，不能添加管道布局，只能添加过滤器。

> 美元符号 `$` 在 PipyJS 里是一个有效的变量名。我们选用这个名称是为了使子管道的定义同一般的变量有所区别。这个也是我们推荐的命名规范，虽然实际上你可以选用任意合法的变量名。

## 测试

现在让我们启动程序并 `curl` 它：

``` sh
curl localhost:8000 -i
```

你将得到如下的错误响应：

```
HTTP/1.1 502 Connection Refused
content-length: 0
connection: keep-alive
```

这是因为我们还没有上游服务器来接收请求。

### 再开启一个 Pipy 实例

我们目前只有一个单一的 Pipy 实例，它已经在运行我们的代理程序。现在需要再开启一个作为测试服务器。

1. 返回 Web 界面主页，找到我们之前创建的 `/hello` 代码库，点击以打开它。

2. 复制显示在浏览器地址栏中的 URL。

3. 再打开一个终端窗口，键入 `pipy`，把刚刚复制的那个 URL 粘贴在后面，然后敲回车。

```sh
pipy http://localhost:6060/repo/hello/
```

现在一个新的 Pipy 实例应该运行起来了。重试一下之前的测试，看看是否得到正确的结果。

```
HTTP/1.1 200 OK
content-length: 11
connection: keep-alive

Hi, there!
```

## 总结

在这部分教程里，你学会了如何让 Pipy 作为一个网络代理。你也明白了为什么我们需要 _子管道_ 以及如何定义子管道布局。

### 要点

1. 端口管道在 TCP 层接收数据。若要处理单独的 HTTP 消息，需要类似 **serveHTTP()** 和 **demuxHTTP()** 这样的过滤器来 _解复用_ 消息。

2. **serveHTTP()** 和 **demuxHTTP()** 的不同之处在于，前者通过 _回调函数_ 来处理解复用出来的消息，而后者在多个分离的 _子管道_ 里面处理。

3. 在 HTTP 消息被发送到一个远程服务器之前，需要通过 **muxHTTP()** _多路复用_ 到单个 TCP 流。

4. 使用 **connect()** 来向外建立一个到远程主机的 TCP 连接。

### 接下来

我们对网络代理的首次尝试是过度简化的，除了把一个 IP/端口映射成另一个之外什么也没做。要让一个代理真正有用，需要具备如下一些功能，譬如路由，负载均衡等等。接下来，我们来看看一个代理的基本工作之一：路由。